package org.karelun.rabbit.bridge;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.eclipse.emf.cdo.view.CDOView;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.command.UnexecutableCommand;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.AbstractTreeIterator;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.edit.command.CommandParameter;
import org.eclipse.emf.edit.command.CopyToClipboardCommand;
import org.eclipse.emf.edit.command.CreateChildCommand;
import org.eclipse.emf.edit.command.CutToClipboardCommand;
import org.eclipse.emf.edit.command.DeleteCommand;
import org.eclipse.emf.edit.command.OverrideableCommand;
import org.eclipse.emf.edit.command.PasteFromClipboardCommand;
import org.eclipse.emf.edit.command.RemoveCommand;
import org.eclipse.emf.edit.command.ReplaceCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.IEditingDomainItemProvider;
import org.eclipse.emf.edit.provider.IWrapperItemProvider;
import org.eclipse.emf.edit.provider.ItemProviderAdapter;

public class TransactionAdapterFactoryEditingDomain implements
		ITransactionEditingDomain {

	public static class TransactionEditingDomainProvider extends AdapterImpl
			implements ITransactionEditingDomainProvider {

		protected ITransactionEditingDomain editingDomain;

		public TransactionEditingDomainProvider(
				ITransactionEditingDomain editingDomain) {
			this.editingDomain = editingDomain;
		}

		@Override
		public ITransactionEditingDomain getTransactionEditingDomain() {
			return editingDomain;
		}

		@Override
		public boolean isAdapterForType(Object type) {
			return type == ITransactionEditingDomainProvider.class;
		}
	}

	public static boolean isStale(Object object) {
		if (object instanceof IWrapperItemProvider) {
			IWrapperItemProvider wrapper = (IWrapperItemProvider) object;
			return isStale(wrapper.getValue()) || isStale(wrapper.getOwner());
		} else if (object instanceof Collection<?>) {
			for (Object item : (Collection<?>) object) {
				if (isStale(item)) {
					return true;
				}
			}
			return false;
		} else if (object instanceof Object[]) {
			for (Object item : (Object[]) object) {
				if (isStale(item)) {
					return true;
				}
			}
			return false;
		} else if (object instanceof EObject) {
			EObject eObject = (EObject) object;
			return eObject.eIsProxy() && eObject.eAdapters().isEmpty();
		} else if (object instanceof FeatureMap.Entry) {
			Object value = ((FeatureMap.Entry) object).getValue();
			return isStale(value);
		} else if (object == null) {
			return false;
		} else {
			Class<? extends Object> objectClass = object.getClass();
			try {
				Method method = objectClass.getMethod("toArray");
				return isStale(method.invoke(object));
			} catch (Exception e) {
				return false;
			}
		}
	}

	public static ITransactionEditingDomain getTransactionEditionDomainFor(
			EObject object) {
		if (object == null) {
			return null;
		}
		Resource resource = object.eResource();
		if (resource == null) {
			return null;
		}
		ITransactionEditingDomainProvider editingDomainProvider = (ITransactionEditingDomainProvider) EcoreUtil
				.getExistingAdapter(resource,
						ITransactionEditingDomainProvider.class);
		if (editingDomainProvider != null) {
			return editingDomainProvider.getTransactionEditingDomain();
		}
		ResourceSet resourceSet = resource.getResourceSet();
		if (resourceSet instanceof ITransactionEditingDomainProvider) {
			ITransactionEditingDomain editingDomain = ((ITransactionEditingDomainProvider) resourceSet)
					.getTransactionEditingDomain();
			return editingDomain;
		} else if (resourceSet != null) {
			editingDomainProvider = (ITransactionEditingDomainProvider) EcoreUtil
					.getExistingAdapter(resourceSet,
							ITransactionEditingDomainProvider.class);
			if (editingDomainProvider != null) {
				return editingDomainProvider.getTransactionEditingDomain();
			}
		}
		return null;
	}

	public static ITransactionEditingDomain getTransactionEditingDomainFor(
			Object object) {
		if (object instanceof ITransactionEditingDomainProvider) {
			return ((ITransactionEditingDomainProvider) object)
					.getTransactionEditingDomain();
		}
		if (object instanceof EObject) {
			return TransactionAdapterFactoryEditingDomain
					.getTransactionEditionDomainFor((EObject) object);
		}
		if (object instanceof FeatureMap.Entry) {
			Object value = ((FeatureMap.Entry) object).getValue();
			return getTransactionEditingDomainFor(value);
		}
		if (object instanceof IWrapperItemProvider) {
			Object value = ((IWrapperItemProvider) object).getValue();
			return getTransactionEditingDomainFor(value);
		}
		return null;
	}

	public static IEditingDomainItemProvider getEditingDomainItemProviderFor(Object object){
		if(object instanceof EObject){
			EObject eObject=(EObject)object;
			ITransactionEditingDomain editingDomain = getTransactionEditionDomainFor(eObject);
			if(editingDomain instanceof TransactionAdapterFactoryEditingDomain){
				object=((TransactionAdapterFactoryEditingDomain)editingDomain).getAdapterFactory().adapt(eObject, IEditingDomainItemProvider.class);
			}
		}
		return object instanceof IEditingDomainItemProvider?
				(IEditingDomainItemProvider)object:
				object instanceof IWrapperItemProvider?
				getEditingDomainItemProviderFor(((IWrapperItemProvider)object).getValue()):
				object instanceof FeatureMap.Entry?
				getEditingDomainItemProviderFor(((FeatureMap.Entry)object).getValue()):null;
					
				
	}

	protected class AdapterFactoryTransactionEditingDomainResourceSet extends
			ResourceSetImpl implements ITransactionEditingDomainProvider {

		public AdapterFactoryTransactionEditingDomainResourceSet() {
			super();
		}

		@Override
		public ITransactionEditingDomain getTransactionEditingDomain() {
			return TransactionAdapterFactoryEditingDomain.this;
		}
	}

	protected AdapterFactory adapterFactory;

	protected CommandStack commandStack;

	protected ResourceSet resourceSet;

	protected Collection<Object> clipboard;

	protected boolean optimizeCopy = true;

	protected Map<Resource, Boolean> resourceToReadOnlyMap;

	private CDOView cdoView;

	public TransactionAdapterFactoryEditingDomain(
			AdapterFactory adapterFactory, CommandStack commandStack,
			CDOView cdoView) {
		this.adapterFactory = adapterFactory;
		this.commandStack = commandStack;
		this.resourceSet = new AdapterFactoryTransactionEditingDomainResourceSet();
		this.cdoView = cdoView;
	}

	public TransactionAdapterFactoryEditingDomain(
			AdapterFactory adapterFactory, CommandStack commandStack,
			Map<Resource, Boolean> resourceToReadOnlyMap, CDOView cdoView) {
		this.adapterFactory = adapterFactory;
		this.commandStack = commandStack;
		this.resourceSet = new AdapterFactoryTransactionEditingDomainResourceSet();
		this.resourceToReadOnlyMap = resourceToReadOnlyMap;
		this.cdoView = cdoView;
	}

	public TransactionAdapterFactoryEditingDomain(
			AdapterFactory adapterFactory, CommandStack commandStack,
			ResourceSet resourceSet, CDOView cdoView) {
		this.adapterFactory = adapterFactory;
		this.commandStack = commandStack;
		this.resourceSet = resourceSet;
		this.cdoView = cdoView;
	}

	public void setCDOView(CDOView cdoView) {
		this.cdoView = cdoView;
	}

	@Override
	public CDOView getCDOView() {
		return cdoView;
	}

	public AdapterFactory getAdapterFactory() {
		return adapterFactory;
	}

	public void setAdapterFactory(AdapterFactory adapterFactory) {
		this.adapterFactory = adapterFactory;
	}

	@Override
	public Resource createResource(String fileNameURI) {
		URI uri = URI.createURI(fileNameURI);
		Resource resource = resourceSet.createResource(uri);
		return resource;
	}

	@Override
	public Resource loadResource(String fileNameURI) {
		URI uri = URI.createURI(fileNameURI);
		Resource resource = resourceSet.getResource(uri, true);
		return resource;
	}

	@Override
	public ResourceSet getResourceSet() {
		return resourceSet;
	}

	@Override
	public Command createCommand(Class<? extends Command> commandClass,
			CommandParameter commandParameter) {
		Object owner = commandParameter.getOwner();
		if (commandClass == CopyToClipboardCommand.class) {
			return new CopyToClipboardCommand(this,
					commandParameter.getCollection());
		} else if (commandClass == PasteFromClipboardCommand.class) {
			return new PasteFromClipboardCommand(this,
					commandParameter.getOwner(), commandParameter.getFeature(),
					commandParameter.getIndex(), getOptimizeCopy());
		} else if (commandClass == CutToClipboardCommand.class) {
			Command removeCommand = RemoveCommand.create(this, commandParameter.getOwner(),commandParameter.getFeature(),commandParameter.getCollection());
			return new CutToClipboardCommand(this, removeCommand);
		} else if (commandClass == DeleteCommand.class) {
			return new DeleteCommand(this, commandParameter.getCollection());
		} else if (owner != null) {
			
			IEditingDomainItemProvider editingDomainItemProvider = 
			        (IEditingDomainItemProvider)
			          adapterFactory.adapt(owner, IEditingDomainItemProvider.class);
			      return
			        editingDomainItemProvider != null ?
			          editingDomainItemProvider.createCommand(owner, this, commandClass, commandParameter) :
			        new ItemProviderAdapter(null).createCommand(owner, this, commandClass, commandParameter);
			
		}else{
			if(commandClass==RemoveCommand.class){
				CompoundCommand removeCommand = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL);

		        List<Object> objects = new ArrayList<Object>(commandParameter.getCollection());
		        while (!objects.isEmpty()){
		          // We will iterate over the whole collection, removing some as we go.
		          //
		          ListIterator<Object> remainingObjects = objects.listIterator();

		          // Take the first object, and remove it.
		          //
		          Object object = remainingObjects.next();
		          remainingObjects.remove();

		          // Determine the object's parent.
		          //
		          Object parent = getParent(object);

		          if (parent != null){
		            // Now we want to find all the other objects with this same parent.
		            // So we can collection siblings together and give the parent control over their removal.
		            //
		            List<Object> siblings = new ArrayList<Object>();
		            siblings.add(object);

		            while (remainingObjects.hasNext()){
		              // Get the next object and check if it has the same parent.
		              //
		              Object otherObject = remainingObjects.next();
		              Object otherParent = getParent(otherObject);
		              if (otherParent == parent){
		                // Remove the object and add it as a sibling.
		                //
		                remainingObjects.remove();
		                siblings.add(otherObject);
		              }
		            }

		            // We will now create a command with this implied parent
		            //
		            removeCommand.append(createCommand(RemoveCommand.class, new CommandParameter(parent, null, siblings)));
		          }
		          else if (object != null){
		            // The parent is null, which implies a top-level removal, so create a self-removing command.
		            //
		            removeCommand.append(createCommand(RemoveCommand.class, new CommandParameter(object, null, Collections.singleton(object))));
		          }
		        } 

		        return removeCommand.unwrap();
		      } 
		      else if (commandClass == ReplaceCommand.class){
		        Object obj = commandParameter.getValue();
		        Object parent = (obj == null) ? null : getParent(obj);
		        if (parent == null) parent = obj;
		        return createCommand(ReplaceCommand.class, new CommandParameter(parent, null, obj, commandParameter.getCollection()));
		      }else if (commandClass == CreateChildCommand.class){
		        // For CreateChildCommand, we will find the owner by calling EditingDomain.getParent() on the first selected object
		        Collection<?> sel = commandParameter.getCollection();
		        Object parent = sel == null ? null : getParent(sel.iterator().next());
		        if (parent == null){
		          return UnexecutableCommand.INSTANCE;
		        }
		        return createCommand(CreateChildCommand.class, new CommandParameter(parent, commandParameter.getFeature(), commandParameter.getValue(), commandParameter.getCollection(), commandParameter.getIndex()));
		      }
		    }

		    try{
		      Constructor<? extends Command> constructor = commandClass.getConstructor(EditingDomain.class, CommandParameter.class);
		      Command command = constructor.newInstance(new Object [] { this, commandParameter });
		      return command;
		    }
		    catch (IllegalAccessException exception){
		      // Ignore.
		    }
		    catch (InstantiationException exception){
		      // Ignore.
		    }
		    catch (NoSuchMethodException exception){
		      // Ignore.
		    }
		    catch (InvocationTargetException exception){
		      // Ignore.
		    }
		    return UnexecutableCommand.INSTANCE;
	}

	@Override
	public Command createOverrideCommand(OverrideableCommand command) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public CommandStack getCommandStack() {
		return commandStack;
	}

	@Override
	public Collection<?> getChildren(Object object) {
		IEditingDomainItemProvider editingDomainItemProvider=(IEditingDomainItemProvider)adapterFactory.adapt(object, IEditingDomainItemProvider.class);
		return editingDomainItemProvider!=null? editingDomainItemProvider.getChildren(object):Collections.emptyList();
	}

	@Override
	public Object getParent(Object object) {
		IEditingDomainItemProvider editingDomainItemProvider=(IEditingDomainItemProvider)adapterFactory.adapt(object, IEditingDomainItemProvider.class);
		
		return editingDomainItemProvider!=null?editingDomainItemProvider.getParent(object):null;
	}

	@Override
	public Object getRoot(Object object) {
		Object result=object;
		for(Object parent=getParent(object);parent!=null;parent=getParent(parent)){
			result=parent;
		}
		return result;
	}
	
	public List<?> resolve(Collection<?> objects)
	  {
	    List<Object> result = new UniqueEList<Object>();
	    for (Object object : objects)
	    {
	      if (isStale(object))
	      {
	        Object unwrappedObject = unwrap(object);
	        if (unwrappedObject instanceof EObject)
	        {
	          EObject resolvedEObject = EcoreUtil.resolve((EObject)unwrappedObject, resourceSet);
	          if (resolvedEObject != unwrappedObject)
	          {
	            result.add(object instanceof IWrapperItemProvider ? getWrapper(resolvedEObject) : resolvedEObject);
	          }
	        }
	      }
	      else
	      {
	        result.add(object);
	      }
	    }
	    return result;
	  }

	public Object getWrapper(Object object)
	  {
	    return getWrapper(object, this);
	  }
	
	public static Object getWrapper(Object object, EditingDomain domain)
	  {
	    if (object != null)
	    {
	      for (Iterator<?> i = domain.treeIterator(domain.getRoot(object)); i.hasNext(); )
	      {
	        Object element = i.next();
	        Object elementValue = element; 
	        while (elementValue instanceof IWrapperItemProvider)
	        {
	          elementValue = ((IWrapperItemProvider)elementValue).getValue();
	        }
	        if (elementValue == object)
	        {
	          return element;
	        }
	        else if (elementValue instanceof FeatureMap.Entry)
	        {
	          Object entryValue = ((FeatureMap.Entry)elementValue).getValue();
	          if (entryValue == object)
	          {
	            return element;
	          }
	        }
	      }
	    }
	    return object;
	  }

	  public static Object unwrap(Object object)
	  {
	    while (object instanceof IWrapperItemProvider)
	    {
	      object = ((IWrapperItemProvider)object).getValue();
	    }
	    if (object instanceof FeatureMap.Entry)
	    {
	      object = ((FeatureMap.Entry)object).getValue();
	    }
	    return object;
	  }
	
	@Override
	public Collection<?> getNewChildDescriptors(Object object, Object sibling) {
		if(object==null){
			object=getParent(object);
		}
		IEditingDomainItemProvider editingDomainItemProvider = 
			      (IEditingDomainItemProvider)
			        adapterFactory.adapt(object, IEditingDomainItemProvider.class);

		return editingDomainItemProvider != null ?
			        editingDomainItemProvider.getNewChildDescriptors(object, this, sibling) :
			        Collections.emptyList();
	}

	@Override
	public Collection<Object> getClipboard() {
		return clipboard;
	}
	
	@Override
	public void setClipboard(Collection<Object> clipboard){
		this.clipboard=clipboard;
	}
	
	@Override
	public boolean getOptimizeCopy() {
		return optimizeCopy;
	}
	
	public void setOptimizeCopy(boolean optimizeCopy) {
		this.optimizeCopy = optimizeCopy;
	}
	
	public Map<Resource, Boolean> getResourceToReadOnlyMap()
	  {
	    return resourceToReadOnlyMap;
	  }

	  public void setResourceToReadOnlyMap(Map<Resource, Boolean> resourceToReadOnlyMap)
	  {
	    this.resourceToReadOnlyMap = resourceToReadOnlyMap;
	  }

	@Override
	public boolean isReadOnly(Resource resource) {
		if (resourceToReadOnlyMap == null)
	    {
	      return false;
	    }
	    else
	    {
	      Boolean result = resourceToReadOnlyMap.get(resource);
	      if (result == null && resource != null)
	      {
	        Map<?, ?> options = Collections.singletonMap(URIConverter.OPTION_REQUESTED_ATTRIBUTES, Collections.singleton(URIConverter.ATTRIBUTE_READ_ONLY));
			Map<String, ?> attributes = (resource.getResourceSet() == null ? resourceSet : resource.getResourceSet()).getURIConverter().getAttributes(resource.getURI(), options);
	        result = Boolean.TRUE.equals(attributes.get(URIConverter.ATTRIBUTE_READ_ONLY));
	        resourceToReadOnlyMap.put(resource, result);
	      }
	      return Boolean.TRUE.equals(result);
	    }
	}

	public static class DomainTreeIterator<E> extends AbstractTreeIterator<E>{

		private static final long serialVersionUID = 1L;
		
		protected ITransactionEditingDomain domain;
		
		public DomainTreeIterator(ITransactionEditingDomain domain, E object) {
			super(object);
			this.domain=domain;
		}
		
		public DomainTreeIterator(ITransactionEditingDomain domain,Object object,boolean include){
			super(object,include);
			this.domain=domain;
		}
		
		@Override
		protected Iterator<? extends E> getChildren(Object object) {
			return (Iterator<E>)domain.getChildren(object).iterator();
		}
	}
	
	@Override
	public TreeIterator<?> treeIterator(Object object) {
		return new DomainTreeIterator<Object>(this, object);
	}
	
	@Override
	public List<?> getTreePath(Object object) {
		LinkedList<Object> result=new LinkedList<Object>();
		result.addFirst(object);
		while((object=getParent(object))!=null){
			result.addFirst(object);
		}
		return result;
	}
	
	@Override
	public boolean isControllable(Object object) {
		if (!(object instanceof EObject)) return false;
	    EObject eObject = (EObject)object;
	    EObject container = eObject.eContainer();
	    return container != null && eObject.eContainmentFeature().isResolveProxies() &&
	      !isReadOnly(eObject.eResource()) && !isReadOnly(container.eResource());
	}

	public static boolean isControlled(Object object)
	  {
	    if (!(object instanceof EObject)) return false;
	    EObject eObject = (EObject)object;
	    EObject container = eObject.eContainer();
	    Resource resource = eObject.eResource();
	    return resource != null && container != null && resource != container.eResource();
	  }
}
